﻿/// <reference path="../Core.ts" />

module Supernova {

    // Desirability constants
    var DESIRABILITY_PER_FULL_GOLD_PLANET = 40;
    var DESIRABILITY_PER_FULL_BARREN_PLANET = 30;
    var DESIRABILITY_PER_FULL_HABITABLE_PLANET = 50;
    var DESIRABILITY_PER_GOODY = 10;

    // Star system constants
    var MAX_STAR_PLACEMENT_ATTEMPTS = 100;

    export class Generator {

        public static generateGalaxy(profile: GeneratorProfile) {

            // Keep regenerating a galaxy until we find one that is acceptable
            do {
                var systemCount = profile.systems.count.maximum;
                console.log('Generating a galaxy containing ' + systemCount + ' star systems.');
                var nestCount = 0;

                // Create a galaxy whose size depends on the number of star systems
                var galaxy = new Galaxy(GameMode.singlePlayer, profile);
                galaxy.diameter = Math.max(1, Math.sqrt((systemCount * 75) / Math.PI) * 2);

                // Set science/gold amounts
                galaxy.science = profile.resources.initialScience;
                galaxy.scienceThreshold = SCIENCE_FIRST_COST;
                galaxy.gold = profile.resources.initialGold;

                // Generate a list of star systems        
                this.generateStarSystems(galaxy, systemCount);

                // Put goodies everywhere
                this.generateGoodies(galaxy);

                // Calculate total desirability for each system so that we can select one for the human player
                var homeSystems = this.selectHomeSystems(galaxy, 1);

                // Assign system to the human based on that most closely matching desirability
                for (var i = 0; i < homeSystems.length; i++) {
                    var home = homeSystems[i];
                    home.system.owner = OwnerType.human;
                    home.system.name = 'Home System';
                    home.system.discovered = true;
                    home.body.maxPopulation = Math.max(profile.home.startingPopulation, home.body.maxPopulation);
                    home.body.population = profile.home.startingPopulation;
                    home.body.facilities.shipyard = true;
                    home.body.facilities.science = true;
                    home.body.goodies.gold = 0;
                    home.body.goodies.science = 0;
                    galaxy.home.systems.push(home.system);
                }

                // Put wrecks around the home system
                this.generateWrecks(galaxy, galaxy.home.systems);

                // Make the computer own all other star systems
                for (var i = 0; i < galaxy.starSystems.length; i++) {
                    var system = galaxy.starSystems[i];
                    for (var j = 1; j < system.bodies.length; j++) {
                        var body = system.bodies[j];
                        if (system.owner != OwnerType.human && body.type == BodyType.habitable) {
                            system.owner = OwnerType.computer;
                            body.population = 1;
                            body.populationProgress = Math.random() * POPULATION_INCREMENT_THRESHOLD;
                            nestCount += 1;
                        }
                    }
                }

                // Set view
                var humanSystem = homeSystems[0].system;
                galaxy.currentView = { x: humanSystem.x, y: humanSystem.y, zoom: MIN_ZOOM_LEVEL };
                galaxy.targetView = { x: humanSystem.x, y: humanSystem.y, zoom: STARTING_ZOOM_LEVEL };

                // Create an AI handler
                this.generateAIHandler(galaxy, humanSystem);

                // Return the galaxy, if it satisfies our requirements
                if (nestCount >= profile.ai.minimumNests) return galaxy;

            } while (true);
        }

        private static generateStarSystems(galaxy: Galaxy, systemCount: number) {

            var info = galaxy.profile.systems;
            for (var i = 0; i < systemCount; i++) {

                // Determine where the star system will be placed, making sure that it isn't going to be too close to another star system
                var location = this.generateStarLocation(galaxy, systemCount);

                // Create the star system
                var system = new StarSystem();
                system.name = info.namingScheme == StarNamingScheme.pretty ? NameGenerator.generateName() : randomInt(1000, 9999).toString();
                system.x = location.x;
                system.y = location.y;

                // Assign the star system a random number of orbiting planets
                var planetCount = randomInt(info.minPlanets, info.maxPlanets);
                if (Math.random() <= info.chanceNoPlanets) {
                    system.name = '';
                    planetCount = 0;
                }
                for (var j = 0; j < planetCount; j++) {
                    var planet = new Body(system);
                    planet.distance = info.minPlanetDistance + Math.random() * (info.maxPlanetDistance - info.minPlanetDistance);
                    planet.initialAngle = Math.PI * 2 * Math.random();
                    planet.maxPopulation = randomInt(1, info.maxPopulation);

                    // Determine the planet type
                    planet.type = BodyType.gas;
                    var selector = Math.random();
                    if (selector < info.chanceBarren) {
                        planet.type = BodyType.barren;
                        system.terraformable = true;
                        if (Math.random() <= galaxy.profile.resources.goldChanceOnBarrenPlanets) {
                            planet.gold = Math.floor(Math.random() * (galaxy.profile.resources.goldMaximum + 1));
                        }
                    } else if (selector >= info.chanceBarren && selector < info.chanceBarren + info.chanceHabitable) {
                        planet.type = BodyType.habitable;
                        system.habitable = true;
                    } else {
                        planet.maxPopulation = 0;
                    }

                    system.bodies[j + 1] = planet;
                }
                galaxy.starSystems[i] = system;
            }
        }

        private static generateStarLocation(galaxy: Galaxy, totalSystemCount: number): IPoint {

            // Try to find a location for the star
            var location: IPoint = null;
            var allowed: boolean = false;
            var infiniteTester = 0;
            var minDistanceBetweenSystems = galaxy.diameter / totalSystemCount;
            do {
                location = randomPointInCircle(galaxy.diameter / 2);
                allowed = true;
                infiniteTester++;
                for (var j = 0; j < galaxy.starSystems.length; j++) {
                    var checkSystem = galaxy.starSystems[j];
                    if (calculateDistance(location.x, location.y, checkSystem.x, checkSystem.y) < minDistanceBetweenSystems) {
                        allowed = false;
                    }
                }
            }
            while (!allowed && infiniteTester <= MAX_STAR_PLACEMENT_ATTEMPTS);

            // Did we have too many problems trying to find a location for the star?
            if (infiniteTester > MAX_STAR_PLACEMENT_ATTEMPTS) {
                console.warn("Tried too many times to place a star in the galaxy.");
            }
            return location;
        }

        private static generateGoodies(galaxy: Galaxy) {
            var info = galaxy.profile.goodies;
            var goodyMinimumCount = info.minPerSystemMultiplier * galaxy.starSystems.length;
            var goodyMaximumCount = info.maxPerSystemMultiplier * galaxy.starSystems.length;
            var goodyCount = Math.floor(randomFloat(goodyMinimumCount, goodyMaximumCount));
            for (var i = 0; i < goodyCount; i++) {
                for (var attemptNumber = 1; attemptNumber <= 25; attemptNumber++) {
                    var system = galaxy.starSystems[randomInt(0, galaxy.starSystems.length - 1)];
                    if (system.bodies.length <= 1) continue;
                    var body = system.bodies[randomInt(1, system.bodies.length - 1)];
                    if (body.population > 0) continue;
                    var selection = Math.random();
                    if (selection < info.chanceGold) {
                        body.goodies.gold = info.values.gold[randomInt(0, info.values.gold.length - 1)];
                    }
                    else {
                        body.goodies.science = info.values.science[randomInt(0, info.values.science.length - 1)];
                    }
                    break;
                }
            }
        }

        private static selectHomeSystems(galaxy: Galaxy, playerCount: number): Array<{ system: StarSystem; body: Body }> {

            // Calculate total desirability for each star system
            var stats: Array<{ s: StarSystem; d: number }> = [];
            for (var i = 0; i < galaxy.starSystems.length; i++) {
                var system = galaxy.starSystems[i];
                var systemDesirability = 0;
                var foundGold = false;

                // Calculate desirability for each body
                for (var j = 1; j < system.bodies.length; j++) {
                    var body = system.bodies[j];

                    // Potential population
                    if (body.type == BodyType.barren) {
                        systemDesirability += (body.maxPopulation / galaxy.profile.systems.maxPopulation) * DESIRABILITY_PER_FULL_BARREN_PLANET;
                    }
                    if (body.type == BodyType.habitable) {
                        systemDesirability += (body.maxPopulation / galaxy.profile.systems.maxPopulation) * DESIRABILITY_PER_FULL_HABITABLE_PLANET;
                    }

                    // Gold
                    if (body.gold > 0) {
                        systemDesirability += (body.gold / galaxy.profile.resources.goldMaximum) * DESIRABILITY_PER_FULL_GOLD_PLANET;
                        foundGold = true;
                    }

                    // Goodies
                    if (body.goodies.gold > 0) systemDesirability += DESIRABILITY_PER_GOODY;
                    if (body.goodies.science > 0) systemDesirability += DESIRABILITY_PER_GOODY;
                }

                // Systems without any gold at all are undesirable
                if (!foundGold) systemDesirability /= 10000;

                // Add to list
                stats.push({ s: system, d: systemDesirability });
            }

            // Determine the target desirability value
            var minimumDesirabilityValue = Number.MAX_VALUE;
            var maximumDesirabilityValue = Number.MIN_VALUE;
            for (var i = 0; i < stats.length; i++) {
                if (stats[i].d < minimumDesirabilityValue) minimumDesirabilityValue = stats[i].d;
                if (stats[i].d > maximumDesirabilityValue) maximumDesirabilityValue = stats[i].d;
            }
            var targetDesirabilityValue = minimumDesirabilityValue + (maximumDesirabilityValue - minimumDesirabilityValue) * galaxy.profile.home.systemDesirabilityPercentile;

            // Create a stack that contains systems whose desirability most closely matches target desirability, in order            
            var unsortedSystems: Array<{ s: StarSystem; d: number }> = stats;
            var sortedSystems: Array<{ s: StarSystem; d: number }> = [];
            while (unsortedSystems.length > 0) {

                // Get system closest to target desirability value, ie that with the smallest difference
                var minDifference = Number.MAX_VALUE;
                var minDifferenceSystem: { s: StarSystem; d: number } = null;
                for (var i = 0; i < unsortedSystems.length; i++) {
                    var info = unsortedSystems[i];
                    var difference = Math.abs(info.d - targetDesirabilityValue);
                    if (difference < minDifference) {
                        minDifference = difference;
                        minDifferenceSystem = info;
                    }
                }

                // Add the next system to the list, so long as it has some desirability at all
                unsortedSystems.splice(unsortedSystems.indexOf(minDifferenceSystem), 1);
                if (minDifferenceSystem.d > 0 && minDifferenceSystem.s.owner == OwnerType.none && minDifferenceSystem.s.habitable) {
                    sortedSystems.push(minDifferenceSystem);
                }
            }

            // Find a home system for each player
            var homeSystems: Array<{ system: StarSystem; body: Body }> = [];
            for (var playerCode = 0; playerCode < playerCount; playerCode++) {

                // Any candidate systems left?
                if (sortedSystems.length <= 0) {
                    throw 'Galaxy generation failed because a system could not be found for a human player to take control of.';
                }

                // Get the next best system from the list
                var playerHomeSystem = sortedSystems.shift().s;

                // Assign one of the planets within the system to be the human player's home planet
                var possibleHomeBodies = [];
                for (var i = 1; i < playerHomeSystem.bodies.length; i++) {
                    var possibleBody = playerHomeSystem.bodies[i];
                    if (possibleBody.type == BodyType.habitable) possibleHomeBodies.push(possibleBody);
                }
                if (possibleHomeBodies.length == 0) {
                    throw 'Galaxy generation failed because a system assigned to a human player does not contain a suitable planet to use as a home planet.';
                }
                homeSystems.push({ system: playerHomeSystem, body: possibleHomeBodies[randomInt(0, possibleHomeBodies.length - 1)] });
            }

            // Return the list
            return homeSystems;
        }

        private static generateWrecks(galaxy: Galaxy, humanSystems: Array<StarSystem>) {

            // How many wrecks are we going to create?
            var info = galaxy.profile.wrecks;
            var playerCount = humanSystems.length;
            var wreckCount = Math.max(info.perHumanPlayer * playerCount, info.perSystem * galaxy.starSystems.length);

            // Create the required number of wrecks
            var wrecks: Array<Wreck> = [];
            var playerCursor = 0;
            for (var i = 0; i < wreckCount; i++) {

                // Create default relative location for wreck
                var location = randomPointInCircle(galaxy.diameter / 2);
                var wreck = new Wreck();
                wreck.quantity = info.upgradesInSystem;

                // Is this wreck inside a human owned system?
                if (i < info.perHumanPlayerNearHomeSystem * playerCount) {
                    var homeSystem = humanSystems[playerCursor];
                    location = randomPointInCircle(galaxy.profile.systems.maxPlanetDistance * info.inSystemDistanceMultiplier);
                    location.x = homeSystem.x + location.x;
                    location.y = homeSystem.y + location.y;
                    playerCursor = (playerCursor + 1) % playerCount;
                } else {

                    // Wrecks outside of the home systems are worth more
                    wreck.quantity = randomInt(info.minUpgradesOutsideSystem, info.maxUpgradesOutsideSystem);
                }

                // Add to the list
                wreck.code = wrecks.length;
                wreck.x = location.x;
                wreck.y = location.y;
                wrecks.push(wreck);
            }
            galaxy.wrecks = wrecks;
        }

        private static generateAIHandler(galaxy: Galaxy, humanSystem: StarSystem) {

            // Get the star system that is closest to the human player, the starting point for spawners
            var closestSystem: StarSystem = null;
            var closestDistance = Number.MAX_VALUE;
            for (var i = 0; i < galaxy.starSystems.length; i++) {
                var possibleClosestSystem = galaxy.starSystems[i];
                var distance = calculateDistance(possibleClosestSystem.x, possibleClosestSystem.y, humanSystem.x, humanSystem.y);
                if (possibleClosestSystem != humanSystem && possibleClosestSystem.owner <= OwnerType.computer && distance < closestDistance) {
                    closestSystem = possibleClosestSystem;
                    closestDistance = distance;
                }
            }

            // Create an AI handler
            var initialSpawningRadius = calculateDistance(closestSystem.x, closestSystem.y, humanSystem.x, humanSystem.y);
            var radiusExpandRate = galaxy.diameter / (60 * 25);
            var info = galaxy.profile.ai.spawn;
            galaxy.ai = new AIHandler(galaxy.profile.ai.spawn.initialWait, initialSpawningRadius, radiusExpandRate, info.timePerLevel, info.minInterval, info.maxInterval, info.progressionFactor);
        }

    }

}